// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';
import 'package:compiler/src/common/elements.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/js_model/js_world.dart' show JClosedWorld;
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:compiler/src/util/memory_compiler.dart';

enum Kind { regular, native, jsInterop }

main() {
  asyncTest(() async {
    await runTest(
      'tests/web/jsinterop_test.dart',
      '',
      {
        'Class': Kind.regular,
        'JsInteropClass': Kind.jsInterop,
        'flag': Kind.regular,
        'topLevelField': Kind.regular,
        'topLevelGetter': Kind.regular,
        'topLevelSetter': Kind.regular,
        'topLevelFunction': Kind.regular,
        'externalTopLevelGetter': Kind.jsInterop,
        'externalTopLevelSetter': Kind.jsInterop,
        'externalTopLevelFunction': Kind.jsInterop,
        'externalTopLevelJsInteropGetter': Kind.jsInterop,
        'externalTopLevelJsInteropSetter': Kind.jsInterop,
        'externalTopLevelJsInteropFunction': Kind.jsInterop,
        'Class.generative': Kind.regular,
        'Class.fact': Kind.regular,
        'Class.instanceField': Kind.regular,
        'Class.instanceGetter': Kind.regular,
        'Class.instanceSetter': Kind.regular,
        'Class.instanceMethod': Kind.regular,
        'Class.staticField': Kind.regular,
        'Class.staticGetter': Kind.regular,
        'Class.staticSetter': Kind.regular,
        'Class.staticMethod': Kind.regular,
        'JsInteropClass.externalGenerative': Kind.jsInterop,
        'JsInteropClass.externalFact': Kind.jsInterop,
        'JsInteropClass.externalJsInteropGenerative': Kind.jsInterop,
        'JsInteropClass.externalJsInteropFact': Kind.jsInterop,
        'JsInteropClass.externalInstanceGetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceSetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceMethod': Kind.jsInterop,
        'JsInteropClass.externalStaticGetter': Kind.jsInterop,
        'JsInteropClass.externalStaticSetter': Kind.jsInterop,
        'JsInteropClass.externalStaticMethod': Kind.jsInterop,
        'JsInteropClass.externalInstanceJsInteropGetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceJsInteropSetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceJsInteropMethod': Kind.jsInterop,
        'JsInteropClass.externalStaticJsInteropGetter': Kind.jsInterop,
        'JsInteropClass.externalStaticJsInteropSetter': Kind.jsInterop,
        'JsInteropClass.externalStaticJsInteropMethod': Kind.jsInterop,
      },
      skipList: [
        // TODO(34174): Js-interop fields should not be allowed.
        '01',
        '02',
        '03',
        '04',
        '38',
        '42',
        '46',
        '51',
        // TODO(33834): Non-external constructors should not be allowed.
        '35',
        '37',
        // TODO(34345): Non-external static members should not be allowed.
        '43',
        '44',
        '45',
        '52',
        '53',
        '54',
      ],
    );
    await runTest(
      'tests/web/non_jsinterop_test.dart',
      '',
      {
        'Class': Kind.regular,
        'JsInteropClass': Kind.jsInterop,
        'flag': Kind.regular,
        'topLevelField': Kind.regular,
        'topLevelGetter': Kind.regular,
        'topLevelSetter': Kind.regular,
        'topLevelFunction': Kind.regular,
        'externalTopLevelJsInteropGetter': Kind.jsInterop,
        'externalTopLevelJsInteropSetter': Kind.jsInterop,
        'externalTopLevelJsInteropFunction': Kind.jsInterop,
        'Class.generative': Kind.regular,
        'Class.fact': Kind.regular,
        'Class.instanceField': Kind.regular,
        'Class.instanceGetter': Kind.regular,
        'Class.instanceSetter': Kind.regular,
        'Class.instanceMethod': Kind.regular,
        'Class.staticField': Kind.regular,
        'Class.staticGetter': Kind.regular,
        'Class.staticSetter': Kind.regular,
        'Class.staticMethod': Kind.regular,
        'JsInteropClass.externalGenerative': Kind.jsInterop,
        'JsInteropClass.externalFact': Kind.jsInterop,
        'JsInteropClass.externalJsInteropGenerative': Kind.jsInterop,
        'JsInteropClass.externalJsInteropFact': Kind.jsInterop,
        'JsInteropClass.externalInstanceGetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceSetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceMethod': Kind.jsInterop,
        'JsInteropClass.externalStaticGetter': Kind.jsInterop,
        'JsInteropClass.externalStaticSetter': Kind.jsInterop,
        'JsInteropClass.externalStaticMethod': Kind.jsInterop,
        'JsInteropClass.externalInstanceJsInteropGetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceJsInteropSetter': Kind.jsInterop,
        'JsInteropClass.externalInstanceJsInteropMethod': Kind.jsInterop,
        'JsInteropClass.externalStaticJsInteropGetter': Kind.jsInterop,
        'JsInteropClass.externalStaticJsInteropSetter': Kind.jsInterop,
        'JsInteropClass.externalStaticJsInteropMethod': Kind.jsInterop,
      },
      skipList: [
        // TODO(34174): Js-interop fields should not be allowed.
        '01',
        '02',
        '03',
        '04',
        '38',
        '42',
        '46',
        '51',
        // TODO(33834): Non-external constructors should not be allowed.
        '35',
        '37',
        // TODO(34345): Non-external static members should not be allowed.
        '43',
        '44',
        '45',
        '52',
        '53',
        '54',
      ],
    );

    await runTest(
      'tests/web/native/native_test.dart',
      'tests/web/native/',
      {
        'Class': Kind.regular,
        'NativeClass': Kind.native,
        'topLevelField': Kind.regular,
        'topLevelGetter': Kind.regular,
        'topLevelSetter': Kind.regular,
        'topLevelFunction': Kind.regular,
        'nativeTopLevelGetter': Kind.native,
        'nativeTopLevelSetter': Kind.native,
        'nativeTopLevelFunction': Kind.native,
        'Class.generative': Kind.regular,
        'Class.fact': Kind.regular,
        'Class.instanceField': Kind.regular,
        'Class.instanceGetter': Kind.regular,
        'Class.instanceSetter': Kind.regular,
        'Class.instanceMethod': Kind.regular,
        'Class.staticField': Kind.regular,
        'Class.staticGetter': Kind.regular,
        'Class.staticSetter': Kind.regular,
        'Class.staticMethod': Kind.regular,
        'Class.nativeInstanceGetter': Kind.native,
        'Class.nativeInstanceSetter': Kind.native,
        'Class.nativeInstanceMethod': Kind.native,
        'NativeClass.generative': Kind.regular,
        'NativeClass.fact': Kind.regular,
        'NativeClass.nativeGenerative': Kind.native,
        'NativeClass.nativeFact': Kind.native,
        'NativeClass.instanceField': Kind.native,
        'NativeClass.instanceNamedField': Kind.native,
        'NativeClass.instanceGetter': Kind.regular,
        'NativeClass.instanceSetter': Kind.regular,
        'NativeClass.instanceMethod': Kind.regular,
        'NativeClass.staticField': Kind.regular,
        'NativeClass.staticGetter': Kind.regular,
        'NativeClass.staticSetter': Kind.regular,
        'NativeClass.staticMethod': Kind.regular,
        'NativeClass.nativeInstanceGetter': Kind.native,
        'NativeClass.nativeInstanceSetter': Kind.native,
        'NativeClass.nativeInstanceMethod': Kind.native,
        'NativeClass.nativeStaticGetter': Kind.native,
        'NativeClass.nativeStaticSetter': Kind.native,
        'NativeClass.nativeStaticMethod': Kind.native,
      },
      skipList: [
        // External constructors in non-native class
        //'08',
        //'09',
        // External instance members in non-native class
        //'22',
        //'23',
        //'24',
        // External static members in non-native class
        //'25',
        //'26',
        //'27',
        // External instance members in native class
        //'36',
        //'37',
        //'38',
        // External static members in native class
        //'39',
        //'40',
        //'41',
      ],
    );
  });
}

runTest(
  String fileName,
  String location,
  Map<String, Kind> expectations, {
  List<String> skipList = const <String>[],
}) async {
  print('--------------------------------------------------------------------');
  print('Testing $fileName');
  print('--------------------------------------------------------------------');
  String test = File(fileName).readAsStringSync();

  List<String> commonLines = <String>[];
  Map<String, SubTest> subTests = <String, SubTest>{};

  int lineIndex = 0;
  for (String line in test.split('\n')) {
    int index = line.indexOf('//#');
    if (index != -1) {
      String prefix = line.substring(0, index);
      String suffix = line.substring(index + 3);
      String name = suffix.substring(0, suffix.indexOf((':'))).trim();
      SubTest subTest = subTests.putIfAbsent(name, () => SubTest());
      subTest.lines[lineIndex] = line;
      int commentIndex = prefix.indexOf('// ');
      if (commentIndex != -1) {
        String combinedErrors = prefix.substring(commentIndex + 3);
        for (String error in combinedErrors.split(',')) {
          subTest.expectedErrors.add(error.trim());
        }
      }
      commonLines.add('');
    } else {
      commonLines.add(line);
    }
    lineIndex++;
  }

  String path = '${location}main.dart';
  Uri entryPoint = Uri.parse('memory:$path');
  await runPositiveTest(entryPoint, {
    path: commonLines.join('\n'),
  }, expectations);
  for (String name in subTests.keys) {
    if (!skipList.contains(name)) {
      SubTest subTest = subTests[name]!;
      await runNegativeTest(subTest, entryPoint, {
        path: subTest.generateCode(commonLines),
      });
    }
  }
}

runPositiveTest(
  Uri entryPoint,
  Map<String, String> sources,
  Map<String, Kind> expectations,
) async {
  CompilationResult result = await runCompiler(
    entryPoint: entryPoint,
    memorySourceFiles: sources,
  );
  Expect.isTrue(result.isSuccess);

  JClosedWorld closedWorld = result.compiler!.backendClosedWorldForTesting!;
  ElementEnvironment elementEnvironment = closedWorld.elementEnvironment;

  void checkClass(
    ClassEntity cls, {
    bool isNative = false,
    bool isJsInterop = false,
  }) {
    if (isJsInterop) {
      isNative = true;
    }
    Expect.equals(
      isJsInterop,
      closedWorld.nativeData.isJsInteropClass(cls),
      "Unexpected js interop class result for $cls.",
    );
    Expect.equals(
      isNative,
      closedWorld.nativeData.isNativeClass(cls),
      "Unexpected native class result for $cls.",
    );
    if (isJsInterop) {
      Expect.isTrue(
        closedWorld.nativeData.isJsInteropLibrary(cls.library),
        "Unexpected js interop library result for ${cls.library}.",
      );
    }
  }

  void checkMember(
    MemberEntity member, {
    bool isNative = false,
    bool isJsInterop = false,
  }) {
    if (isJsInterop) {
      isNative = true;
    }
    Expect.equals(
      isJsInterop,
      closedWorld.nativeData.isJsInteropMember(member),
      "Unexpected js interop member result for $member.",
    );
    Expect.equals(
      isNative,
      closedWorld.nativeData.isNativeMember(member),
      "Unexpected native member result for $member.",
    );
    if (isJsInterop) {
      Expect.isTrue(
        closedWorld.nativeData.isJsInteropLibrary(member.library),
        "Unexpected js interop library result for ${member.library}.",
      );
    }
  }

  elementEnvironment.forEachLibraryMember(elementEnvironment.mainLibrary!, (
    MemberEntity member,
  ) {
    if (member == elementEnvironment.mainFunction) return;

    Kind? kind = expectations.remove(member.name);
    Expect.isNotNull(kind, "No expectations for $member");
    checkMember(
      member,
      isNative: kind == Kind.native,
      isJsInterop: kind == Kind.jsInterop,
    );
  });

  elementEnvironment.forEachClass(elementEnvironment.mainLibrary!, (
    ClassEntity cls,
  ) {
    Kind? kind = expectations.remove(cls.name);
    Expect.isNotNull(kind, "No expectations for $cls");
    checkClass(
      cls,
      isNative: kind == Kind.native,
      isJsInterop: kind == Kind.jsInterop,
    );

    checkClassMember(MemberEntity member) {
      Kind? kind = expectations.remove('${cls.name}.${member.name}');
      Expect.isNotNull(kind, "No expectations for $member");
      checkMember(
        member,
        isNative: kind == Kind.native,
        isJsInterop: kind == Kind.jsInterop,
      );
    }

    elementEnvironment.forEachConstructor(cls, checkClassMember);
    elementEnvironment.forEachLocalClassMember(cls, checkClassMember);
  });

  Expect.isTrue(expectations.isEmpty, "Untested expectations: $expectations");
}

runNegativeTest(
  SubTest subTest,
  Uri entryPoint,
  Map<String, String> sources,
) async {
  DiagnosticCollector collector = DiagnosticCollector();
  CompilationResult result = await runCompiler(
    entryPoint: entryPoint,
    memorySourceFiles: sources,
    diagnosticHandler: collector,
  );
  Expect.isFalse(
    result.isSuccess,
    "Expected compile time error(s) for\n$subTest",
  );
  List<String> expected =
      subTest.expectedErrors.map((error) => 'MessageKind.' + error).toList();
  List<String> actual =
      collector.errors.map((error) => error.messageKind.toString()).toList();
  expected.sort();
  actual.sort();
  Expect.listEquals(
    expected,
    actual,
    "Unexpected compile time error(s) for\n$subTest",
  );
}

class SubTest {
  List<String> expectedErrors = [];
  final Map<int, String> lines = <int, String>{};

  String generateCode(List<String> commonLines) {
    StringBuffer sb = StringBuffer();
    int i = 0;
    while (i < commonLines.length) {
      if (lines.containsKey(i)) {
        sb.writeln(lines[i]);
      } else {
        sb.writeln(commonLines[i]);
      }
      i++;
    }
    return sb.toString();
  }

  @override
  String toString() {
    return lines.values.join('\n');
  }
}
